﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Crm2011.Framework.Core;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;



namespace ScriptEase
{
    /// <summary>
    /// A Project is a working context that is used to connect with a CRM Organization
    /// and manage a local <see cref="ScriptEase.Workspace" />
    /// </summary>
    public sealed class Project : IDisposable
    {
        #region Static Load/Save


        public static ProjectConfiguration NewProject()
        {
            ProjectConfiguration result;
            // look in current bindir
            if (File.Exists("ScriptEase.DefaultTemplate.seproj"))
            {
                string configFileContent = File.ReadAllText(@"ScriptEase.DefaultTemplate.seproj", Encoding.UTF8);
                result = SerializationHelper.DeserializeDc<ProjectConfiguration>(configFileContent);
            }
            else
            {
                result = new ProjectConfiguration
                {
                    AutoUpdate = true,
                    AutoPublish = false,
                    BaseDirectory = @".\{ProjectName}_scripts",
                    MappingFileReaderContractName = "Default"
                };
            }
            result.ConnectionId = Guid.NewGuid();
            return result;
        }



        public static ProjectConfiguration LoadFromFile(string projectConfigurationFile)
        {
            Trace.Default.Info("Loading Project from {0}..", projectConfigurationFile);
            string configFileContent = File.ReadAllText(projectConfigurationFile, Encoding.UTF8);
            ProjectConfiguration result = SerializationHelper.DeserializeDc<ProjectConfiguration>(configFileContent);
            //result.MappingFileName = "abc.xml";
            //configFileContent = SerializationHelper.SerializeDc(result);
            //File.WriteAllText(projectConfigurationFile,configFileContent, Encoding.UTF8);
            result.FilePath = projectConfigurationFile;
            if (result.ConnectionId == Guid.Empty) result.ConnectionId = Guid.NewGuid();
            return result;
        }



        public static void SaveToFile(string filename, ProjectConfiguration data)
        {
            if (!string.IsNullOrEmpty(data.BaseDirectory) && data.BaseDirectory.Contains("{ProjectName}"))
                data.BaseDirectory = data.BaseDirectory.Replace("{ProjectName}", Path.GetFileNameWithoutExtension(filename));

            data.FilePath = filename;
            SaveToFile(data);
        }



        public static void SaveToFile(ProjectConfiguration data)
        {
            File.WriteAllText(data.FilePath, SerializationHelper.SerializeDc(data), Encoding.UTF8);
        }


        #endregion


        private CrmOrganization _crmOrganization;
        private IOrganizationService _orgService;
        private CrmRequestDispatcher _crmRequestDispatcher;

        private readonly Workspace _workspace;
        private readonly string _projectDirectory ;
        private readonly string _staticMappingFilePath;


        /// <summary>
        /// Initializes a new instance of the <see cref="Project" /> 
        /// based on the provided configuration data.
        /// </summary>
        public Project(ProjectConfiguration data)
        {
            this.Data = data;
            _projectDirectory = Path.GetDirectoryName(this.Data.FilePath);
            Trace.Default.Info("Opening project from directory='{0}'", _projectDirectory);

            var workspaceMappers = new List<IWorkspaceMapper>();
            if (!String.IsNullOrWhiteSpace(this.Data.MappingFileName))
            {
                Trace.Default.Info("Considering mapping file='{0}'", this.Data.MappingFileName);
                _staticMappingFilePath = Workspace.GetFullPath(this.Data.MappingFileName, _projectDirectory);
                workspaceMappers.Add(new StaticMap(_staticMappingFilePath, _projectDirectory));
            }
            workspaceMappers.Add(new DirectMap(getPublisherPrefixes()));


            _workspace = new Workspace( _projectDirectory, data.BaseDirectory, localFileChanged, workspaceMappers, data.ReadonlyManaged);
        }



        /// <summary>
        /// Connect with a CRM Organization, download all WebResources 
        /// and setup a local <see cref="ScriptEase.Workspace"/>.
        /// </summary>
        public void Connect(CrmConnectionString connectionString)
        {
            Trace.Default.Info("Connecting to CRM-Server {0}", connectionString.Server);

            _orgService = ServiceFactory.GetOrganizationService(connectionString);
            _crmOrganization = new CrmOrganization(_orgService);
            _crmRequestDispatcher = new CrmRequestDispatcher(_orgService);

            Trace.Default.Debug("Getting Solutions from CRM..");
            this.Solutions = _crmOrganization.GetSolutions();
            Trace.Default.Debug("Getting Web-Resources from CRM..");
            var webResources = _crmOrganization.GetWebResources(this.Data.IncludeManaged, this.Data.IncludeNonCustomizable).ToList();
            Trace.Default.Debug("Initializing Web-Resource mapping..");
            _workspace.InitializeMap(webResources);

            this.ConnectionString = connectionString;
        }



        /// <summary>
        /// Disconnects from CRM and close the local <see cref="ScriptEase.Workspace"/>.
        /// </summary>
        public void Disconnect()
        {
            _workspace.CloseMap();

            this.ConnectionString = null;
            this.Solutions = null;
            _crmRequestDispatcher = null;
            _crmOrganization = null;
            _orgService = null;
        }



        private void disposeManaged()
        {
            this.Disconnect();
        }



        /// <summary>
        /// Get the <see cref="ProjectConfiguration"/>.
        /// </summary>
        /// <remarks>
        /// Use <see cref="LoadFromFile"/> to load configuration data from a file.
        /// </remarks>
        public ProjectConfiguration Data { get; private set; }


        /// <summary>
        /// Gets the project directory.
        /// </summary>
        /// <remarks>
        /// This is the deirectory where the project file is located.
        /// </remarks>
        /// <seealso cref="LoadFromFile"/>
        public string ProjectDirectory { get { return _projectDirectory; } }
        
        public string StaticMappingFilePath { get { return _staticMappingFilePath; } }



        /// <summary>
        /// Gets the name of the project.
        /// </summary>
        /// <remarks>
        /// The name of the project is the name of the project configuration file without its extension.
        /// </remarks>
        public string ProjectName { get { return Path.GetFileNameWithoutExtension(this.Data.FilePath); } }


        /// <summary>
        /// Gets the connection string of a connected project, <c>null</c> if project is not connected.
        /// </summary>
        public CrmConnectionString ConnectionString { get; private set; }



        /// <summary>
        /// All CRM Solutions available in the connected CRM Organization.
        /// </summary>
        /// <remarks>
        /// <c>null</c> if the Project is not connected.
        /// </remarks>
        public IEnumerable<Solution> Solutions { get; private set; }



        public MappingList GetMappings()
        {
            return _workspace.Mappings;
        }


        private void localFileChanged(object sender, FileChangedEventArgs e)
        {
            MappingModel mapping = _workspace.Mappings.GetMapping(e.FileInfo.FullName);

            mapping.SyncStatus = FileSyncStatus.PendingUpdate;
            if (mapping.AutoUpdate && this.Data.AutoUpdate)
            {
                this.UpdateWebResource(mapping);
            }
        }



        /// <summary>
        /// Disable ynchronization of all mappings which are not in the selected solution
        /// </summary>
        /// <param name="solutionUniqueName">Name of the solution unique.</param>
        public void FilterBySolution(string solutionUniqueName)
        {
            if (this.Solutions == null) return;

            var currentSolution = this.Solutions.FirstOrDefault(s => String.Equals(s.UniqueName, solutionUniqueName, StringComparison.InvariantCultureIgnoreCase));
            if (currentSolution == null) return;

            _workspace.Filter(_crmOrganization.GetSolutionWebResourceComponents(currentSolution.Id));
        }



        /// <summary>
        /// Download WebResources from CRM and save them to the local <see cref="ScriptEase.Workspace"/>.
        /// </summary>
        /// <param name="webResourceNames">A list of web resource names to be downloaded and saved</param>
        /// <param name="cr">A handler to resolve conflicts with existing local files.</param>
        public void UpdateLocal(IEnumerable<string> webResourceNames, ConflictResolver.ResolutionHandler cr)
        {
            foreach (string webResourceName in webResourceNames)
            {
                WebResource latestWebResource = _crmOrganization.GetWebResource(webResourceName);
                if (latestWebResource == null)
                    Trace.Default.Warn("WebResource {0} does not exist (anymore) and cannot be downloaded!", webResourceName);
                else
                    _workspace.UpdateLocalFile(latestWebResource, cr);
            }
        }



        /// <summary>
        /// Update a WebResource in the connected CRM Organization.
        /// </summary>
        public void UpdateWebResource(MappingModel mapping)
        {
            Trace.Default.Info("Start update {0}", mapping.WebResourceName);

            WebResource webResource = new WebResource
            {
                Id = mapping.WebResourceId,
                Content = _workspace.GetFile(mapping),
                EntityState = EntityState.Changed
            };

            _crmRequestDispatcher.AddRequest(new UpdateRequest {Target = webResource}, onUpdateRequestCompleted, mapping);
            mapping.SyncStatus = FileSyncStatus.Updating;
        }



        /// <summary>
        /// Publishe a WebResource in the connected CRM Organization.
        /// </summary>
        public void PublishWebResource(MappingModel mapping)
        {
            Trace.Default.Info("Start publish {0}", mapping.WebResourceName);
            _crmRequestDispatcher.AddRequest(new PublishXmlRequest {ParameterXml = getPublishXmlRequest(mapping.WebResourceId)}, onPublishRequestCompleted, mapping);
            mapping.SyncStatus = FileSyncStatus.Publishing;
        }



        #region private helpers


        private void onUpdateRequestCompleted(OrganizationResponse response, object addData, Exception result)
        {
            MappingModel mapping = (MappingModel) addData;

            if (result != null)
            {
                Trace.Default.Error(result);
                mapping.SyncStatus = FileSyncStatus.PendingUpdate; // revert status
                return;
            }

            // update success
            mapping.SyncStatus = FileSyncStatus.PendingPublish;
            if (this.Data.AutoPublish)
                this.PublishWebResource(mapping);
        }



        private void onPublishRequestCompleted(OrganizationResponse response, object addData, Exception result)
        {
            MappingModel mapping = (MappingModel) addData;


            if (result != null)
            {
                Trace.Default.Error(result);
                mapping.SyncStatus = FileSyncStatus.PendingPublish; // revert status
                return;
            }

            // update success
            // Delete *.CRM file if any
            string mergeFilePath = ConflictResolver.GetMergeFileName(Workspace.GetFullPathExists(mapping.FileRelativePath, _workspace.WebResourcesBasePath));
            if (File.Exists(mergeFilePath)) File.Delete(mergeFilePath);
            mapping.SyncStatus = FileSyncStatus.Published;
        }



        private static string getPublishXmlRequest(Guid id)
        {
            return string.Format(
                "<importexportxml><webresources><webresource>{0}</webresource></webresources></importexportxml>", id);
        }



        /// <summary>
        /// Convert the <see cref="ProjectConfiguration.RemovePublisherPrefixes">configured list of publisher prefixes</see>
        /// into an Array.
        /// </summary>
        private IEnumerable<string> getPublisherPrefixes()
        {
            return String.IsNullOrEmpty(this.Data.RemovePublisherPrefixes)
                ? new string[0]
                : this.Data.RemovePublisherPrefixes.Split(';');
        }


        #endregion


        #region IDisposable


        public void Dispose()
        {
            dispose(true);
            //GC.SuppressFinalize(this);
        }



        private void dispose(bool disposing)
        {
            if (!this._disposed)
            {
                if (disposing)
                    disposeManaged();
                //CloseHandle(handle);
            }
            _disposed = true;
        }



        private bool _disposed;


        #endregion


        /// <summary>
        /// Determines whether the current projectz was already connected with the specified CRM-System,
        /// if the conenction string is a <c>known</c> connenction for this project.
        /// </summary>
        /// <param name="connectionString">The connection string.</param>
        /// <returns>
        ///   <c>true</c> if the project has already been connected to the system or 
        /// if the project was not yet conencted to any system; 
        /// otherwise, <c>false</c>.
        /// </returns>
        public bool IsKnownConnection(CrmConnectionString connectionString)
        {
            return this.Data.KnownOrganizations == null ||
                   this.Data.KnownOrganizations.Any(orgUrl => connectionString.OrganizationUrl.Equals(orgUrl, StringComparison.InvariantCultureIgnoreCase));
        }



        /// <summary>
        /// Adds the connection to the list of know connections for this project.
        /// </summary>
        public void SetKnownConnection(CrmConnectionString connectionString)
        {
            if (this.Data.KnownOrganizations == null) this.Data.KnownOrganizations = new List<string>();

            if (!IsKnownConnection(connectionString))
            {
                this.Data.KnownOrganizations.Add(connectionString.OrganizationUrl);
                SaveToFile(this.Data);
            }
        }
    }
}